﻿using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Threading;
using log4net;
using OdsServer.Common;
using OdsServer.Common.DataTransfer;
using OdsServer.Common.Packet;
using OdsServer.Common.Utils;

namespace OdsServer.DataTransfer.Sockets
{
    internal sealed class SocketTransfer: IDataPublisherAgent, IListenerHost
    {
        private static readonly ILog log            = LogManager.GetLogger(typeof(SocketTransfer));
        private volatile bool threadQuit            = false;
        private static List<EndPoint> clientList    = new List<EndPoint>();
        private readonly int port                   = -1;

        private IAsyncResult listenerResult;
        private IPEndPoint localEP;

        internal Thread listenerThread;
        internal volatile Socket server;

        public event DataPublisherFaultDelegate OnFault;

        public SocketTransfer(IServerConfiguration serverConfiguration) 
        {
            if (serverConfiguration == null)
                throw new ArgumentNullException("serverConfiguration cannot be null");
            if (serverConfiguration.SocketTransfer == null)
                throw new ArgumentNullException("serverConfiguration.SocketTransfer cannot be null");

            this.port = serverConfiguration.SocketTransfer.Port;
        }

        ~SocketTransfer()
        {
            this.StopAgent();
        }

        public bool IsActive
        {
            get
            {
                lock (this)
                {
                    return (this.listenerThread != null
                            && this.listenerThread.IsAlive);
                }
            }
        }

        public void StartAgent()
        {
            listenerThread              = new Thread(new ThreadStart(HostService));
            listenerThread.IsBackground = true;
            listenerThread.Priority     = ThreadPriority.Normal;

            listenerThread.SetApartmentState(ApartmentState.MTA);

            threadQuit = false;

            listenerThread.Start();
        }

        private void HostService()
        {
            IPAddress ipv4  = DataTransferUtils.GetMachineIP();
            localEP         = new IPEndPoint(ipv4, port);

            server = new Socket(
                    AddressFamily.InterNetwork,
                    SocketType.Dgram,
                    ProtocolType.Udp );

            server.Blocking = true;
            server.DontFragment = true;
            server.Bind(localEP);

            this.StartListening();

            // if out of StartListening loop, then a critical error occured
            if (!threadQuit)
            {
                this.StopService(false);
                this.StartAgent();
            }
        }

        public void StopAgent()
        {
            this.StopService(true);
        }

        private void StopService(bool waitThread)
        {
            this.threadQuit = true;

            if (this.IsActive)
            {
                if (waitThread)
                    listenerThread.Join();

                if (server != null)
                {
                    lock (server)
                    {
                        this.RemoveAllListeners();

                        server.Shutdown(SocketShutdown.Both);
                        server.Close();

                        server = null;
                    }
                }
            }
        }

        // dummy stub to pretend async use due to framework bug
        void ReceiveData(IAsyncResult iar)
        {
        }

        private unsafe void StartListening()
        {
            EndPoint remoteEP   = new IPEndPoint(IPAddress.Any, 0);
            int recv            = 0;
            byte[] inputBuf     = new byte[2048];

            while (!this.threadQuit)
            {
                recv = 0;
                /*
                 * There is a known bug in synchronous implementation of Socket.ReceiveFrom
                 * See: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=97539
                 * 
                 */
                try
                {
                    lock (inputBuf)
                    {
#if !WIN_2K
                        listenerResult = server.BeginReceiveFrom(
                                inputBuf,
                                0,
                                inputBuf.Length,
                                SocketFlags.None,
                                ref remoteEP,
                                new AsyncCallback(ReceiveData),
                                server );

                        // double check sleeping 10ms inbetween
                        while (listenerResult.AsyncWaitHandle.WaitOne(10) != true) 
                        {
                            if (this.threadQuit)
                                return;
                        }
                        recv = server.EndReceiveFrom(listenerResult, ref remoteEP);
#else
                        recv = server.ReceiveFrom(
                                inputBuf,
                                0,
                                inputBuf.Length,
                                SocketFlags.None,
                                ref remoteEP);
#endif
                    }
                }
                catch (SocketException se)
                {
                    log.ErrorFormat(
                            "StartListening / Socket exception occured: {0} while receiving data from: {1}",
                            se.Message,
                            remoteEP);
                    this.RemoveListener(remoteEP);

                    lock (server)
                    {
                        switch(se.SocketErrorCode)
                        {
                            case SocketError.ConnectionAborted:
                            case SocketError.ConnectionRefused:
                            case SocketError.ConnectionReset:
                                // TODO: try to reset connection
#if !WIN_2K
                                // shutting down receive for the first time
                                if (listenerResult.IsCompleted)
                                    server.Shutdown(SocketShutdown.Receive);
#else
                                server.Shutdown(SocketShutdown.Receive);
#endif
                                break;
                            case SocketError.Shutdown:
                            case SocketError.OperationAborted:
                                // TODO: try to reinit

                                // looks like there is a serious underlying trouble, stopping execution
                                this.ReportFault(new DataTransferException("Critical fault occured, exiting operation thread."));

                                return;
                        }
                    }
                }

                try
                {
                    IBasePacket request = PacketFactory.DeserializePacket(inputBuf);

                    if (request is CommandPacket)
                    {
                        CommandPacket clientCommand = (CommandPacket)request;
                        CommandPacket response;

                        switch (clientCommand.GetCommand())
                        {
                            case DataTransferCommand.Connect:
                                response = new CommandPacket(DataTransferCommand.ConnectAccepted);
                                this.Send(remoteEP, response);
                                AddListener(remoteEP);
                                break;
                            case DataTransferCommand.Disconnect:
                                response = new CommandPacket(DataTransferCommand.DisconnectAcknowledged);
                                this.Send(remoteEP, response);
                                RemoveListener(remoteEP);
                                break;
                        }
                    } // ignore otherwise  
                }
                catch (SerializationException se)
                {
                    log.WarnFormat(
                            "Packet appears to be invalid: {0} Received from: {1}",
                            se.Message,
                            remoteEP );
                }
            }
        }

        public void Send(EndPoint addr, IBasePacket packet)
        {
            try
            {
                lock (server)
                {
                    server.SendTo(
                        DataTransferUtils.RawSerialize(packet),
                        Marshal.SizeOf(packet),
                        SocketFlags.None,
                        addr );
                }
            }
            catch (SocketException se)
            {
                log.ErrorFormat(
                        "Send / Socket exception occured: {0} while sending data to: {1}",
                        se.Message,
                        addr );
            }
            catch (NullReferenceException)
            {
                throw new DataTransferException("Server object not initiated");
            }
        }

        public void AddListener(object listener)
        {
            if (listener != null 
                    && listener is EndPoint)
            {
                EndPoint ep = (EndPoint)listener;
                lock (clientList)
                {
                    if (!clientList.Contains(ep))
                        clientList.Add(ep);
                }
            }
        }

        public void RemoveListener(object listener)
        {
            if (listener != null
                    && listener is EndPoint)
            {
                EndPoint ep = (EndPoint)listener;
                lock (clientList)
                {
                    if (clientList.Contains(ep))
                        clientList.Remove(ep);
                }
            }
        }

        public void RemoveAllListeners()
        {
            CommandPacket response = new CommandPacket(DataTransferCommand.DisconnectAcknowledged);

            // Lock server object at higher level only
            foreach (EndPoint endPoint in clientList)
            {
                lock (server)
                {
                    server.SendTo(
                            DataTransferUtils.RawSerialize(response),
                            Marshal.SizeOf(response),
                            SocketFlags.None,
                            endPoint );
                }
            }
            clientList.Clear();
        }

        public void Publish(object sender, IBasePacket data)
        {
            if (clientList != null
                    && clientList.Count != 0)
            {
                lock (clientList)
                {
                    foreach (EndPoint endPoint in clientList)
                    {
                        try
                        {
                            lock (server)
                            {
                                server.SendTo(
                                        DataTransferUtils.RawSerialize(data),
                                        Marshal.SizeOf(data),
                                        SocketFlags.None,
                                        endPoint);
                            }
                        }
                        catch (SocketException se)
                        {
                            log.ErrorFormat(
                                    "Publish / Socket exception occured: {0} while receiving data from: {1}",
                                    se.Message,
                                    endPoint);
                            clientList.Remove(endPoint);
                        }
                        catch (NullReferenceException)
                        {
                            throw new DataTransferException("Server object not initiated");
                        }
                    }
                }
            }
        }

        public void ReportFault(DataTransferException e)
        {
            if (this.OnFault != null)
                this.OnFault(this, e);
        }
    }
}